/**
 *
 * 四川大学 2021-2022学年秋季学期 《计算机网络》编程作业
 *
 * Packet InterNet Grouper P I N G
 *
 **/

#include "ping.h"
#include <asm-generic/socket.h>
#include <netinet/ip_icmp.h>

// 共发送4个数据包
#define N_PACKETS 4
// 缓冲区大小
#define BUFFER_SIZE 1024
// ICMP数据长度为32B
#define ICMP_DATA_SIZE 32
// ICMP头部长56字节
#define ICMP_HEADER_SIZE 56
// 总长度为二者之和
#define ICMP_PACKET_SIZE (ICMP_DATA_SIZE + ICMP_HEADER_SIZE)
// 超时时间，秒数+微秒数
#define TIMEOUT_S 1
#define TIMEOUT_US 0

// 发送报文与接受报文的缓冲区
char g_Sendbuf[BUFFER_SIZE], g_Recvbuf[BUFFER_SIZE];
// 保存数据包的RTT信息
double g_Rtt[N_PACKETS];
// 记录接收到数据包数
int g_Received = 0;
// socket文件描述符
int g_Sock = -1;
// 发送时间和接受时间，用于计算RTT
struct timeval g_Sendtime, g_Recvtime;
// ICMP id
int g_Icmp_id = 10402;
// 发送与接收的超时
const struct timeval timeout = {TIMEOUT_S, TIMEOUT_US};
// 目标地址
struct sockaddr_in dest_addr;

/**
 * @brief 检查系统函数的返回值，是否出错
 *
 * @param ret 系统函数的返回值
 * @param msg 出错显示消息
 */
static inline void check_ret(int ret, const char * msg) {
  if(ret < 0) {
    perror("[ERROR]");
    puts(msg);
    _exit(0);
  }
}

/**
 * @brief 开始PING循环
 *
 * @param host 主机名（域名或IP地址）
 */
void start_ping(const char *host) {
  // 创建一个IP ICMP socket
  g_Sock = socket(AF_INET, SOCK_RAW, IPPROTO_ICMP);
  // 设置发送超时
  setsockopt(g_Sock, SOL_SOCKET, SO_SNDTIMEO, &timeout, sizeof(struct timeval));
  // 设置接收超时
  setsockopt(g_Sock, SOL_SOCKET, SO_RCVTIMEO, &timeout, sizeof(struct timeval));

  // 检测socket是否成功创建
  check_ret(g_Sock, "Create socket failed, please run as root");

  // 解析域名
  int inaddr;
  if ((inaddr = inet_addr(host)) == INADDR_NONE) {
    // 以点分十进制解析参数失败，尝试作为域名解析
    struct hostent *pHostent = gethostbyname(host);
    if (pHostent == NULL) {
      // 解析域名失败
      herror("gethostbyname failed");
      _exit(-1);
    }
    memcpy(&dest_addr.sin_addr, pHostent->h_addr_list[0], pHostent->h_length);
  } else {
    // 以点分十进制解析参数成功
    memcpy(&dest_addr.sin_addr, &inaddr, sizeof(struct in_addr));
  }

  // 主循环：发送数据包
  for (int i = 0; i < N_PACKETS; ++i) {
    g_Rtt[i] = -1;
    send_packet(i);
    recv_packet(i);

    if (g_Rtt[i] < 1000) {  // 时间在1s之内
      unpack_packet(i);
      // printf("%ld, %ld\n", (long)g_Recvtime.tv_sec,
      // (long)g_Recvtime.tv_usec);
      // 休眠，使两次发送数据包时间间隔在1s
      long unsigned sleeptime =
          1000000 - ((g_Recvtime.tv_sec) * 1000000 + (g_Recvtime.tv_usec));
      // printf("%lu\n", sleeptime);
      // if(g_Rtt[i] != -1) usleep(sleeptime);
      usleep(sleeptime);
    }
  }

    // 输出统计信息
    // 计算最大值、最小值、平均值、标准差
    double Max = -1, Min = 1e6, Avg = 0, Mdev = 0;
    for(int i=0; i<N_PACKETS; ++i) {
      if(g_Rtt[i] >= 1000) continue;
      double rtt = g_Rtt[i];
      Max = Max > rtt ? Max : rtt;
      Min = Min < rtt ? Min : rtt;
      Avg += rtt;
    }
    Avg /= g_Received;
    for(int i=0; i<N_PACKETS; ++i) {
      if(g_Rtt[i] >= 1000) continue;
      double diff = g_Rtt[i] - Avg;
      Mdev += diff*diff;
    }
    Mdev /= g_Received;
    Mdev = sqrt(Mdev);

    // 输出
    printf("\n--- %s ping stastistics ---\n", host);
    // 如果没有接收到数据包，则不输出最大值等信息
    if(g_Received) printf("rtt max: %.2lf ms, min: %.2lf ms, avg: %.2lf ms, mdev: %.2lf ms\n", Max, Min, Avg, Mdev);
    printf("%d packets transmitted, %d received, %d dropped, %.2lf%% packet loss\n", N_PACKETS, g_Received,
        N_PACKETS - g_Received, 100 * (1 - (double)g_Received/N_PACKETS));
    // 关闭socket
    close(g_Sock);
}

/**
 * @brief 计算one's complement of one's complement sum of the ICMP packet
 *
 * @param packet 指向数据包的指针
 * @param len 数据包长度
 * @return uint16_t 计算得到的16位校验码
 */
uint16_t internet_checksum(struct icmp *packet, uint32_t len) {
    uint16_t *data = (uint16_t *)packet;
    uint32_t sum = 0;

    while (len > 1)
        sum += *data++, len -= 2;
    if (len == 1) {
        uint8_t *last = (uint8_t *)data;
        sum += ((uint16_t)(*last) << (uint32_t)16);
    }
    while (sum >> 16) {
        sum = (sum >> 16) + (sum & 0x0000ffff);
    }
    sum = ~sum;
    return sum;
}

/**
 * @brief 以seq为序列号发送报文
 *
 * @param seq 序列号
 * @return int sendto的返回值
 */
int send_packet(int seq) {
    struct icmp *packet = (struct icmp *)g_Sendbuf;

    packet->icmp_type = ICMP_ECHO;
    packet->icmp_code = 0;
    packet->icmp_cksum = 0;
    packet->icmp_seq = seq;
    packet->icmp_id = g_Icmp_id;
    for(int i=0; i<ICMP_DATA_SIZE; ++i) {
        packet->icmp_data[i] = i;
    }
    packet->icmp_cksum = internet_checksum(packet, ICMP_PACKET_SIZE);

    gettimeofday(&g_Sendtime, NULL);
    int ret = sendto(g_Sock, packet, ICMP_PACKET_SIZE, 0,
        (struct sockaddr *)&dest_addr, sizeof(struct sockaddr_in));
    return ret;
}

/**
 * @brief 计算RTT
 *
 * @param start 开始时间
 * @param end 结束时间
 * @return double RTT
 * @notice 该函数会改变end，end指向的结构体中将包含准确的时间差
 */
double computeRTT(struct timeval *start, struct timeval *end) {
  if ((end->tv_usec -= start->tv_usec) < 0) {
    // 进位
    --(end->tv_sec);
    end->tv_usec += 1000000;
  }
  end->tv_sec -= start->tv_sec;

  double rtt = end->tv_sec * 1000.0 + end->tv_usec / 1000.0;
  return rtt;
}

/**
 * @brief 接收数据包
 *
 * @param seq 待接收数据包的序列号
 * @return int 接收到的数据量
 */
int recv_packet(int seq) {
  int recv_bytes = 0;
  unsigned int addrlen = sizeof(struct sockaddr_in);

  recv_bytes = recvfrom(g_Sock, g_Recvbuf, BUFFER_SIZE, 0,
      (struct sockaddr *)&dest_addr, &addrlen);
  gettimeofday(&g_Recvtime, NULL);
  // check_ret(recv_bytes, "RECV failed");
  if(recv_bytes > 0) {
    // puts("SUCCESS");
    g_Received ++;
  }
  else {
    puts("Request time out.");
  }

  g_Rtt[seq] = computeRTT(&g_Sendtime, &g_Recvtime);

  return recv_bytes;
}

/**
 * @brief 解析数据包
 *
 * @param seq 数据包序号
 */
void unpack_packet(int seq) {
  struct ip*ipPacket = (struct ip*) g_Recvbuf;

  int ipHeaderLen = ipPacket->ip_hl << 2; // Header Length * 4
  struct icmp *icmpPacket =
      (struct icmp *)(g_Recvbuf + ipHeaderLen);  // get ICMP for the payload
  if(icmpPacket->icmp_type == ICMP_ECHOREPLY &&
      icmpPacket->icmp_id == g_Icmp_id) {
    // puts("RECEIVED");

    // unpack
    unsigned int ret_seq = icmpPacket->icmp_seq;
    unsigned int ttl = ipPacket->ip_ttl; // TTL is a field of IP packet
    const char *ip_src = inet_ntoa(ipPacket->ip_src);
    unsigned int recvDataLen = ntohs(ipPacket->ip_len) - ipHeaderLen;
    double rtt = g_Rtt[seq];

    printf("%u bytes from %s: icmp_seq=%u, ttl=%u, time=%.2f ms\n",
        recvDataLen, ip_src, ret_seq, ttl, rtt);
  }
}
